package removed;

import java.net.InetAddress;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.ListIterator;
import java.util.Timer;

import javax.sip.*;
import javax.sip.address.*;
import javax.sip.header.*;
import javax.sip.message.Request;
import javax.sip.message.Response;

import edu.thu.thss.yxy.sip.*;
import edu.thu.thss.yxy.sip.event.RegistrationStateChangeEvent;
import edu.thu.thss.yxy.util.Logger;

/**
 * Contains all functionality that has anything to do with registering and
 * maintaining registrations with a SIP Registrar.
 * 
 * @author Andy Gao
 * 
 */
public class SipRegistrarProcessor {
	/**
	 * Our class logger.
	 */
	private static final Logger logger = Logger
			.getLogger(SipRegistrarProcessor.class);

	/**
	 * A reference to the sip provider that created us.
	 */
	private SipManager sipMgrCallback = null;

	/**
	 * The SipURI containing the address port and transport of our registrar
	 * server.
	 */
	private SipURI registrarURI = null;

	/**
	 * The InetAddress of the registrar we are connecting to.
	 */
	private InetAddress registrarAddress = null;

	/**
	 * The default amount of time (in seconds) that registration take to expire
	 * or otherwise put - the number of seconds we wait before re- registering.
	 */
	public static final int DEFAULT_REGISTRATION_EXPIRATION = 3600;

	/**
	 * The amount of time (in seconds) that registration take to expire or
	 * otherwise put - the number of seconds we wait before re-registering.
	 */
	private int registrationsExpiration = DEFAULT_REGISTRATION_EXPIRATION;

	/**
	 * Keeps our current registration state.
	 */
	private RegistrationState currentRegistrationState = RegistrationState.UNREGISTERED;

	/**
	 * A copy of our last sent register request. (used when unregistering)
	 */
	private Request registerRequest = null;
	
	/**
	 * The client transaction that we used for sending the last REGISTER
	 * request.
	 */
	ClientTransaction regTrans = null;

	/**
	 * The next long to use as a cseq header value.
	 */
	private long nextCSeqValue = 1;

	/**
	 * callId must be unique from first register to last one, till we
	 * unregister.
	 */
	private CallIdHeader callIdHeader = null;

	/**
	 * Empty constructor
	 */
	protected SipRegistrarProcessor() {

	}

	/**
	 * Creates a new instance of this class.
	 * 
	 * @param registrarAddress
	 *            the ip address or FQDN of the registrar we will be registering
	 *            with.
	 * @param registrarPort
	 *            the port on which the specified registrar is accepting
	 *            connections.
	 * @param registrationTransport
	 *            the transport to use when sending our REGISTER request to the
	 *            server.
	 * @param expirationTimeout
	 *            the number of seconds to wait before re-registering.
	 * @param sipProviderCallback
	 *            a reference to the ProtocolProviderServiceSipImpl instance
	 *            that created us.
	 * 
	 * @throws ParseException
	 *             in case the specified registrar address is not a valid
	 *             reigstrar address.
	 */
	public SipRegistrarProcessor(InetAddress registrarAddress,
			int registrarPort, String registrationTransport,
			int expirationTimeout, SipManager sipMgrCallback)
			throws ParseException {
		this.sipMgrCallback = sipMgrCallback;
		this.registrarAddress = registrarAddress;
		registrarURI = sipMgrCallback.getAddressFactory().createSipURI(null,
				registrarAddress.getHostName());

		if (registrarPort != ListeningPoint.PORT_5060)
			registrarURI.setPort(registrarPort);

		registrarURI.setTransportParam(registrationTransport);
		this.registrationsExpiration = expirationTimeout;
	}

	public SipRegistrarProcessor(SipManager sipMgrCallback) {
		this.sipMgrCallback = sipMgrCallback;
	}

	/**
	 * Sends the REGISTER request to the server specified in the constructor.
	 * 
	 * @throws OperationFailedException
	 *             with the corresponding error code if registration or
	 *             construction of the Register request fail.
	 */
	void register() throws OperationFailedException {
		// skip REGISTERING event if we are already registered
		// we are refreshing our registration
		if (getRegistrationState() != RegistrationState.REGISTERED)
			setRegistrationState(RegistrationState.REGISTERING,
					RegistrationStateChangeEvent.REASON_NOT_SPECIFIED, null);

		Request request;
		try {
			// We manage the Call ID Header ourselves.The rest will be handled
			// by our SipMessageFactory
			if (callIdHeader == null)
				callIdHeader = this.getJainSipProvider().getNewCallId();
			
			//From
	        FromHeader fromHeader = sipMgrCallback.getFromHeader();
	        Address fromAddress = fromHeader.getAddress();

	        //CSeq Header
	        CSeqHeader cSeqHeader = sipMgrCallback.getHeaderFactory()
	            .createCSeqHeader(getNextCSeqValue(), Request.REGISTER);


	        //To Header (In the case of SIP Communicator To and From are always
	        //equal.
	        ToHeader toHeader = sipMgrCallback.getHeaderFactory().createToHeader(
	        		fromAddress, null);

	        //MaxForwardsHeader
	        MaxForwardsHeader maxForwardsHeader = sipMgrCallback.getMaxForwardsHeader();

	        //create a host-only uri for the request uri header.
	        String domain = ((SipURI) toHeader.getAddress().getURI()).getHost();

	        //Request URI
	        SipURI requestURI = sipMgrCallback.getAddressFactory()
	                .createSipURI(null, domain);

	        //Via
	        ArrayList<ViaHeader> viaHeaders = sipMgrCallback.getLocalViaHeaders(requestURI);

	        //Request
	        request = sipMgrCallback.getMessageFactory().createRequest(
	                requestURI, Request.REGISTER, callIdHeader, cSeqHeader,
	                fromHeader, toHeader, viaHeaders, maxForwardsHeader);

	        //Expires Header - try to generate it twice in case there was something
	        //wrong with the value we received from the provider or the server.
	        ExpiresHeader expHeader = null;
	        for (int retry = 0; retry < 2; retry++)
	        {
	            try
	            {
	                expHeader = sipMgrCallback.getHeaderFactory()
	                    .createExpiresHeader(registrationsExpiration);
	            }
	            catch (InvalidArgumentException ex)
	            {
	                if (retry == 0)
	                {
	                    registrationsExpiration = 3600;
	                    continue;
	                }
	                throw new IllegalArgumentException(
	                    "Invalid registrations expiration parameter - "
	                    + registrationsExpiration, ex);
	            }
	        }
	        request.addHeader(expHeader);

	        //Add an "expires" param to our Contact header.
	        ContactHeader contactHeader
	            = (ContactHeader)request.getHeader(ContactHeader.NAME);

	        //add expires in the contact header as well in case server likes it
	        //better there.
	        contactHeader.setExpires(registrationsExpiration);

	        request.setHeader(contactHeader);
		} catch (Exception exc) {
			// catches InvalidArgumentException, ParseExeption
			// this should never happen so let's just log and bail.
			logger.error("Failed to create a Register request.", exc);
			setRegistrationState(RegistrationState.CONNECTION_FAILED,
					RegistrationStateChangeEvent.REASON_INTERNAL_ERROR, exc
							.getMessage());
			throw new OperationFailedException(
					"Failed to generate a from header for our register request.",
					OperationFailedException.INTERNAL_ERROR, exc);
		}

		// Transaction
		try {
			regTrans = getJainSipProvider().getNewClientTransaction(request);
		} catch (TransactionUnavailableException ex) {
			logger.error("Could not create a register transaction!\n"
					+ "Check that the Registrar address is correct!", ex);

			setRegistrationState(RegistrationState.CONNECTION_FAILED,
					RegistrationStateChangeEvent.REASON_INTERNAL_ERROR, ex
							.getMessage());

			throw new OperationFailedException(
					"Could not create a register transaction!\n"
							+ "Check that the Registrar address is correct!",
					OperationFailedException.NETWORK_FAILURE, ex);
		}

		try {
			regTrans.sendRequest();
			logger.debug("sent request=\n" + request);
		}
		// we sometimes get a null pointer exception here so catch them all
		catch (Exception ex) {
			logger.error("Could not send out the register request!", ex);
			setRegistrationState(RegistrationState.CONNECTION_FAILED,
					RegistrationStateChangeEvent.REASON_INTERNAL_ERROR, ex
							.getMessage());
			throw new OperationFailedException(
					"Could not send out the register request!",
					OperationFailedException.NETWORK_FAILURE, ex);
		}

		this.registerRequest = request;
	}
	
	 /**
	    * Sends a unregistered request to the registrar thus ending our
	    * registration.
	    * @throws OperationFailedException with the corresponding code if sending
	    * or constructing the request fails.
	    */
	    public void unregister() throws OperationFailedException
	    {
	    	if (getRegistrationState() == RegistrationState.UNREGISTERED)
	        {
	            logger.trace("Trying to unregister when already unresgistered");
	            return;
	        }

	        if (this.registerRequest == null)
	        {
	            logger.error("Couldn't find the initial register request");
	            setRegistrationState(RegistrationState.CONNECTION_FAILED
	                , RegistrationStateChangeEvent.REASON_INTERNAL_ERROR
	                , "Could not find the initial regiest request.");
	            throw new OperationFailedException(
	                "Could not find the initial register request."
	                , OperationFailedException.INTERNAL_ERROR);
	        }

	        setRegistrationState(RegistrationState.UNREGISTERING,
	                RegistrationStateChangeEvent.REASON_USER_REQUEST, "");

	        //We are apparently registered so send an un-Register request.
	        Request unregisterRequest;
	        try
	        {
	        	unregisterRequest = (Request) registerRequest.clone();

	            unregisterRequest.getExpires().setExpires(0);
	            CSeqHeader cSeqHeader = (CSeqHeader) unregisterRequest
	                .getHeader(CSeqHeader.NAME);

	            //[issue 1] - increment registration cseq number
	            //reported by - Roberto Tealdi <roby.tea@tin.it>
	            cSeqHeader.setSeqNumber(getNextCSeqValue());

	            //remove the branch id.
	            ViaHeader via
	                = (ViaHeader)unregisterRequest.getHeader(ViaHeader.NAME);
	            if(via != null)
	                via.removeParameter("branch");


	            //also set the expires param in the contact header in case server
	            //prefers it this way.
	            ContactHeader contact
	                = (ContactHeader)unregisterRequest.getHeader(ContactHeader.NAME);

	            contact.setExpires(0);
	        }
	        catch (InvalidArgumentException ex)
	        {
	            logger.error("Unable to create an unREGISTER request.", ex);
	            //Shouldn't happen
	            setRegistrationState(
	                RegistrationState.CONNECTION_FAILED
	                , RegistrationStateChangeEvent.REASON_INTERNAL_ERROR
	                , "Unable to set Expires Header");
	            throw new OperationFailedException(
	                "Unable to set Expires Header"
	                , OperationFailedException.INTERNAL_ERROR
	                , ex);
	        }

	        ClientTransaction unregisterTransaction = null;
	        try
	        {
	            unregisterTransaction = getJainSipProvider()
	                .getNewClientTransaction(unregisterRequest);
	        }
	        catch (TransactionUnavailableException ex)
	        {
	            logger.error("Unable to create a unregister transaction", ex);
	            setRegistrationState(
	                RegistrationState.CONNECTION_FAILED
	                , RegistrationStateChangeEvent.REASON_INTERNAL_ERROR
	                , "Unable to create a unregister transaction");
	            throw new OperationFailedException(
	                "Unable to create a unregister transaction"
	                , OperationFailedException.INTERNAL_ERROR
	                , ex);
	        }
	        try
	        {
	            // remove current call-id header
	            // on next register we will create new one
	            callIdHeader = null;

	            unregisterTransaction.sendRequest();
	            logger.info("sent request:\n" + unregisterRequest);

	            //if we're currently registered or in a process of unregistering
	            //we'll wait for an ok response before changing the status.
	            //otherwise we set it immediately.
	            if(!(getRegistrationState().equals(RegistrationState.REGISTERED) ||
	            getRegistrationState().equals(RegistrationState.UNREGISTERING)))
	            {
	                logger.info("Setting state to UNREGISTERED.");
	                setRegistrationState(
	                    RegistrationState.UNREGISTERED
	                    , RegistrationStateChangeEvent.REASON_USER_REQUEST, null);

	                //kill the registration tran in case it is still active
	                if (regTrans != null
	                    && regTrans.getState().getValue()
	                            <= TransactionState.PROCEEDING.getValue())
	                {
	                    logger.trace("Will try to terminate reg tran ...");
	                    regTrans.terminate();
	                    logger.trace("Transaction terminated!");
	                }
	            }
	        }
	        catch (SipException ex)
	        {
	            logger.error("Failed to send unregister request", ex);
	            setRegistrationState(
	                RegistrationState.CONNECTION_FAILED
	                , RegistrationStateChangeEvent.REASON_INTERNAL_ERROR
	                , "Unable to create a unregister transaction");
	            throw new OperationFailedException(
	                "Failed to send unregister request"
	                , OperationFailedException.INTERNAL_ERROR
	                , ex);
	        }
	    }
	
	/**
	    * An ok here means that our registration has been accepted or terminated
	    * (depending on the corresponding REGISTER request). We change state
	    * notify listeners and (in the case of a new registration) schedule
	    * reregistration.
	    *
	    * @param clientTransatcion the ClientTransaction that we created when
	    * sending the register request.
	    * @param response the OK Response that we've just received.
	    */
	    @SuppressWarnings("unchecked") 
	    public void processOK(ClientTransaction clientTransatcion,
	                        Response          response)
	    {
	        //first extract the expires value that we requested
	        int requestedExpiration = 0;
	        Request register = clientTransatcion.getRequest();

	        //first check for an expires param in the contact header
	        ContactHeader contactHeader = (ContactHeader) register
	            .getHeader(ContactHeader.NAME);
	        if (contactHeader != null)
	            requestedExpiration = contactHeader.getExpires();
	        else
	            requestedExpiration = 0;

	        //Try the expires header only if there was no expiration interval in
	        //the contact address. (Bug report and fix thereof Frederic Fournier)
	        if(requestedExpiration <= 0)
	        {
	            ExpiresHeader expiresHeader = register.getExpires();
	            if (expiresHeader != null)
	                requestedExpiration = expiresHeader.getExpires();
	        }


	        //now check if the registrar has touched our expiration timeout in its
	        //response. Again, check contact headers first.
	        int grantedExpiration = -1;

	        ContactHeader responseContactHdr = (ContactHeader) response.getHeader(
	            ContactHeader.NAME);
	        if (responseContactHdr != null)
	        {
	            grantedExpiration = responseContactHdr.getExpires();
	        }

	        //no luck there, try the expires header.
	        ExpiresHeader expiresHeader = response.getExpires();

	        if (expiresHeader != null)
	        {
	            grantedExpiration = expiresHeader.getExpires();
	        }
	        //still no luck - let's be tolerant and reuse the expires timeout we
	        //stated in our last request
	        else
	        {
	            grantedExpiration = requestedExpiration;
	        }

	        //If this is a response to a REGISTER request ending our registration
	        //then expires would be 0.
	        //fix by Luca Bincoletto <Luca.Bincoletto@tilab.com>

	        //we also take into account the requested expiration since if it was 0
	        //we don't really care what the server replied (I have an asterisk here
	        //that gives me 3600 even if I request 0).
	        if (grantedExpiration <= 0 || requestedExpiration <= 0)
	        {
	            setRegistrationState(RegistrationState.UNREGISTERED
	                , RegistrationStateChangeEvent.REASON_USER_REQUEST
	                , "Registration terminated.");
	        }
	        else
	        {
	           setRegistrationState(
	                RegistrationState.REGISTERED
	                , RegistrationStateChangeEvent.REASON_NOT_SPECIFIED
	                , null);
	        }
	    }

	/**
	 * Processes a retransmit or expiration Timeout of an underlying
	 * {@link Transaction}handled by this SipListener.
	 * 
	 * @param timeoutEvent
	 *            the timeoutEvent received indicating either the message
	 *            retransmit or transaction timed out.
	 * @return <tt>true</tt> if the specified event has been handled by this
	 *         processor and shouldn't be offered to other processors registered
	 *         for the same method; <tt>false</tt>, otherwise
	 */
	public boolean processTimeout(TimeoutEvent timeoutEvent) {
		if (getRegistrationState().equals(RegistrationState.UNREGISTERED) == false)
        {
            setRegistrationState(RegistrationState.CONNECTION_FAILED,
                RegistrationStateChangeEvent.REASON_NOT_SPECIFIED,
                "A timeout occurred while trying to connect to the server.");
        }
        return true;
	}

	/**
	 * Process an asynchronously reported IO Exception.
	 * 
	 * @param exceptionEvent
	 *            The Exception event that is reported to the application.
	 * @return <tt>true</tt> if the specified event has been handled by this
	 *         processor and shouldn't be offered to other processors registered
	 *         for the same method; <tt>false</tt>, otherwise
	 */
	public boolean processIOException(IOExceptionEvent exceptionEvent) {
		setRegistrationState(RegistrationState.CONNECTION_FAILED,
				RegistrationStateChangeEvent.REASON_NOT_SPECIFIED,
				"An error occurred while trying to connect to the server."
						+ "[" + exceptionEvent.getHost() + "]:"
						+ exceptionEvent.getPort() + "/"
						+ exceptionEvent.getTransport());
		return true;
	}
	
	
	    
	    /**
	     * Handles a NOT_IMPLEMENTED response sent in reply of our register request.
	     *
	     * @param transatcion the transaction that our initial register request
	     * belongs to.
	     * @param response our initial register request.
	     */
	     public void processNotImplemented(ClientTransaction transatcion,
	                                     Response response)
	     {
	             setRegistrationState(
	                 RegistrationState.CONNECTION_FAILED
	                 , RegistrationStateChangeEvent.REASON_NOT_SPECIFIED
	                 , registrarAddress.getHostAddress()
	                 + " does not appear to be a sip registrar. (Returned a "
	                 +"NOT_IMPLEMENTED response to a register request)");
	     }

	/**
	 * Sets our registration state to <tt>newState</tt> and dispatches an event
	 * through the protocol provider service impl.
	 * <p>
	 * 
	 * @param newState
	 *            a reference to the RegistrationState that we're currently
	 *            detaining.
	 * @param reasonCode
	 *            one of the REASON_XXX error codes specified in
	 *            {@link RegistrationStateChangeEvent}.
	 * @param reason
	 *            a reason String further explaining the reasonCode.
	 */
	void setRegistrationState(RegistrationState newState, int reasonCode,
			String reason) {
		if (currentRegistrationState.equals(newState))
			return;

		RegistrationState oldState = currentRegistrationState;
		this.currentRegistrationState = newState;

		sipMgrCallback.fireRegistrationStateChanged(oldState, newState,
				reasonCode, reason);
	}

	/**
	 * Returns the JAIN SIP provider that should be used for communication with
	 * our current registrar.
	 * 
	 * @return the JAIN SIP provider that should be used for communication with
	 *         our current registrar.
	 */
	public SipProvider getJainSipProvider() {
		return sipMgrCallback.getJainSipProvider();
	}
	
	 /**
	    * Returns the state of this connection.
	    * @return a RegistrationState instance indicating the state of our
	    * registration with the corresponding registrar.
	    */
	    public RegistrationState getRegistrationState()
	    {
	        return currentRegistrationState;
	    }

	/**
	 * Returns the address of this connection's registrar.
	 * 
	 * @return the InetAddress of our registrar server.
	 */
	private InetAddress getRegistrarAddress() {
		return registrarAddress;
	}

	/**
	 * Returns the next long to use as a cseq header value.
	 * 
	 * @return the next long to use as a cseq header value.
	 */
	private long getNextCSeqValue() {
		return nextCSeqValue++;
	}

}
